{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "# <center> Write Python Web Framework </center>\n",
    "<br>\n",
    "<center>![web](http://7ktuty.com1.z0.glb.clouddn.com/t.jpg)</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "# <center>WSGI(Web Server Gateway Interface)</center>\n",
    "\n",
    "- [PEP 3333 -- Python Web Server Gateway Interface v1.0.1](https://www.python.org/dev/peps/pep-3333/)\n",
    "- 解决 python web server 乱象 mod_python, CGI, FastCGI, etc.\n",
    "- 描述了web server(Gunicorn, uWSGI等)如何与 web application(flask, django等)交互、web application 如何处理请求\n",
    "- 正是有了 WSGI 规范，我们才能在任意 http web server 上跑各种 web 应用\n",
    "- The WSGI interface has two sides: the \"server\" or \"gateway\" side, and the \"application\" or \"framework\" side"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "# <center> WSGI API (Application side)</center>\n",
    "\n",
    "``` python\n",
    "def application(environ, start_response)\n",
    "```\n",
    "\n",
    "\n",
    "- application 就是 WSGI app，一个可调用对象\n",
    "- 参数：\n",
    "    - environ: 一个包含 WSGI 环境信息的字典，由 WSGI 服务器提供，常见的 key 有 PATH_INFO，QUERY_STRING 等\n",
    "    - start_response: 生成 WSGI 响应的回调函数，接收两个参数，status 和 headers\n",
    "- 函数返回响应体的可迭代对象"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "# <center>一个简单的 app 示例</center>\n",
    "\n",
    "``` python\n",
    "def application(environ, start_response):\n",
    "    status = '200 OK'\n",
    "    headers = \n",
    "        ('Content-Type', 'text/html; charset=utf8')\n",
    "    ]\n",
    "    start_response(status, headers)\n",
    "    return  [b\"<h1>Hello, World!</h1>\"]\n",
    "```\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "outputs": [],
   "source": [
    "\n",
    "\"\"\"我们用 python 内置的 wsgi server 来跑这个应用：(0_demo_wsgi_app.py)\"\"\"\n",
    "\n",
    "\n",
    "# 导入 python内置的 WSGI server\n",
    "from wsgiref.simple_server import make_server\n",
    "\n",
    "def application(environ, start_response):\n",
    "    print(environ)    # 建议打出来这个字典看看有哪些参数\n",
    "    status = '200 OK'\n",
    "    headers = [('Content-Type', 'text/html; charset=utf8')]\n",
    "    start_response(status, headers)\n",
    "    return [b\"<h1>Hello, World!</h1>\"]    # WSGI applications return iterables that yield bytes\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    httpd = make_server('127.0.0.1', 8000, application)\n",
    "    httpd.serve_forever()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "# <center>make_server 如何工作</center>\n",
    "\n",
    "```python\n",
    "def make_server(\n",
    "    host, port, app,  server_class=WSGIServer,\n",
    "    handler_class=WSGIRequestHandler\n",
    "):\n",
    "    \"\"\"Create a new WSGI server\"\"\"\n",
    "    server = server_class(\n",
    "        (host, port), handler_class\n",
    "    )\n",
    "    server.set_app(app)\n",
    "    return server\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "![继承图](http://7ktuty.com1.z0.glb.clouddn.com/wsgi.png)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "outputs": [],
   "source": [
    "    # wsgiref.handlers.BaseHandler\n",
    "    def run(self, application):\n",
    "        \"\"\"Invoke the application\"\"\"\n",
    "        # Note to self: don't move the close()!  Asynchronous servers shouldn't\n",
    "        # call close() from finish_response(), so if you close() anywhere but\n",
    "        # the double-error branch here, you'll break asynchronous servers by\n",
    "        # prematurely closing.  Async servers must return from 'run()' without\n",
    "        # closing if there might still be output to iterate over.\n",
    "        try:\n",
    "            self.setup_environ()\n",
    "            self.result = application(self.environ, self.start_response)\n",
    "            self.finish_response()\n",
    "        except:\n",
    "            try:\n",
    "                self.handle_error()\n",
    "            except:\n",
    "                # If we get an error handling an error, just give up already!\n",
    "                self.close()\n",
    "                raise   # ...and let the actual server figure it out.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "# <center>常用的 environ </center>\n",
    "\n",
    "\n",
    "| Key            | Contents                                                                    |\n",
    "|----------------|-----------------------------------------------------------------------------|\n",
    "| REQUEST_METHOD | 请求方法                                                                    |\n",
    "| PATH_INFO      | 请求路径，比如 /foo/bar/                                                    |\n",
    "| QUERY_STRING   | GET 请求参数，比如 foo=bar&bar=spam，我们可以从这个变量中获取用户的请求参数 |\n",
    "| HTTP_{HEADER}  | http 头信息，比如 HTTP_HOST 等                                              |\n",
    "| wsgi.input     | 包含请求内容的类文件对象(file-like object)，比如 post 请求数据              |\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "# <center> start_response 可调用对象</center>\n",
    "\n",
    "``` python\n",
    "start_response(status, headers)\n",
    "\"\"\"\n",
    "status: 一个包含 http 状态码和描述的字符串, 比如 '200 OK'\n",
    "headers: 一个包含 http 头的元祖列表，[('Content-Type', 'text/html; charset=utf8')]\n",
    "\"\"\"\n",
    "```\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "最后 WSGI 应用返回一个可迭代的 bytes 序列，比如\n",
    "\n",
    "``` python\n",
    "# 注意返回的 bytes 编码要符合你指定的返回头\n",
    "def app(environ, start_response):\n",
    "    # ...\n",
    "    return [b'hello', b'world']\n",
    "\n",
    "def app(environ, start_response):\n",
    "    # ...\n",
    "    yield b'hello'\n",
    "    yield b'world'\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "# <center>使用 environ 获取查询参数 </center>\n",
    "到这里我们就知道如何编写一个最简单的 WSGI 应用了，我们做个简单的练习，当用户访问 http://localhost:8000/?name=John 的时候， 在网页上输出 \"Hello John\"。代码如下：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "outputs": [],
   "source": [
    "from wsgiref.simple_server import make_server\n",
    "\n",
    "\n",
    "def application(environ, start_response):\n",
    "    # print(environ)\n",
    "    status = '200 OK'\n",
    "    headers = [('Content-Type', 'text/html; charset=utf8')]\n",
    "\n",
    "    query_string = environ['QUERY_STRING']    # 这里是 \"name=John\"\n",
    "    name = query_string.split(\"=\")[1]    # 从查询字符串 \"name=John\" 里获取 \"John\"\n",
    "    start_response(status, headers)\n",
    "    return [b\"<h1>Hello, {}!</h1>\".format(name)]\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    httpd = make_server('127.0.0.1', 8000, application)\n",
    "    httpd.serve_forever()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "# <center>抽象 Request 和 Response 对象 </center>\n",
    "\n",
    "前面看到我们可以直接编写符合 WSGI 规范的应用，但是要做的工作量比较多，比如我们得直接去处理 query string，大部分 web 框架会抽象出 Request/Response 对象，这样一个 web 应用看起来会像这样：\n",
    "\n",
    "``` python\n",
    "from somewhere import Response\n",
    "\n",
    "def application(request):\n",
    "    # ...\n",
    "    return Response('blablabla')\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "source": [
    "这样做的好处就是概念上更加清晰，测试更加容易，大部分的 web 框架都采用了类似抽象。接下来我们看看 web 框架是如何映射 WSGI 到 Request/Response 对象的。代码结构将类似这样：\n",
    "\n",
    "``` python\n",
    "def request_response_application(function):\n",
    "    def application(environ, start_response):\n",
    "    # ...\n",
    "    return application\n",
    "\n",
    "@request_response_application\n",
    "def application(request):\n",
    "    # ...\n",
    "    return Response(...)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "# <center>实现 Request </center>\n",
    "\n",
    "实现思路：把 environ 作为构造函数的参数传过去，这样我们就能利用各种子函数来获取我们需要的值。 比如请求地址(HTTP_HOST)，请求参数(QUERY_STRING) 等。比如我们可以用一个函数把 QUERY_STRING 字符串解析后作为 请求参数字典返回，这样使用的时候就方便很多。\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "outputs": [],
   "source": [
    "## -*- coding: utf-8 -*-\n",
    "\n",
    "from six.moves import urllib\n",
    "\n",
    "\n",
    "class Request(object):\n",
    "    def __init__(self, environ):\n",
    "        self.environ = environ\n",
    "\n",
    "    @property\n",
    "    def args(self):\n",
    "        \"\"\" 把查询参数转成字典形式 \"\"\"\n",
    "        get_arguments = urllib.parse.parse_qs(\n",
    "            self.environ['QUERY_STRING']\n",
    "        )\n",
    "        return {k: v[0] for k, v in get_arguments.items()}\n",
    "    \n",
    "    @property\n",
    "    def path(self):\n",
    "        return self.environ['PATH_INFO']"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "# <center>实现 Response </center>\n",
    "\n",
    "Response 对象需要返回的内容大概如下:\n",
    "\n",
    "- 返回内容\n",
    "- 状态码\n",
    "- 字符编码\n",
    "- 返回类型\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "outputs": [],
   "source": [
    "import http.client\n",
    "from six.moves import urllib\n",
    "from wsgiref.headers import Headers\n",
    "\n",
    "class Response(object):\n",
    "    def __init__(self, response=None, status=200, \n",
    "                 charset='utf-8', content_type='text/html'):\n",
    "        self.response = [] if response is None else response\n",
    "        self.charset = charset\n",
    "        self.headers = Headers()\n",
    "        content_type = '{content_type}; charset={charset}'.format(\n",
    "            content_type=content_type, charset=charset)\n",
    "        self.headers.add_header('content-type', content_type)\n",
    "        self._status = status\n",
    "\n",
    "    @property\n",
    "    def status(self):\n",
    "        status_string = http.client.responses.get(self._status, 'UNKNOWN')\n",
    "        return '{status} {status_string}'.format(\n",
    "            status=self._status, status_string=status_string)\n",
    "\n",
    "    def __iter__(self):\n",
    "        for val in self.response:\n",
    "            if isinstance(val, bytes):\n",
    "                yield val\n",
    "            else:\n",
    "                yield val.encode(self.charset)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "# <center>转换函数</center>\n",
    "现在 Request/Response 对象都有了，还差一个转换函数，用来把之前的 WSGI 函数转换成使用我们的 Request/Response 对象的函数，我们写个装饰器来实现这个功能:\n",
    "\n",
    "``` python\n",
    "def request_response_application(func):\n",
    "    def application(environ, start_response):\n",
    "        request = Request(environ)\n",
    "        response = func(request)\n",
    "        start_response(\n",
    "            response.status,\n",
    "            response.headers.items()\n",
    "        )\n",
    "        return iter(response)\n",
    "    return application\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "outputs": [],
   "source": [
    "# 试试结合了 Resquest 和 Response 的新 application:(1_request_response_demo.py)\n",
    "\n",
    "@request_response_application\n",
    "def application(request):\n",
    "    name = request.args.get('name', 'default_name')    # 获取查询字符串中的 name\n",
    "    return Response(['<h1>hello {name}</h1>'.format(name=name)])\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    httpd = make_server('127.0.0.1', 8000, application)\n",
    "    httpd.serve_forever()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "#  <center>使用 Werkzeug </center>\n",
    "其实如果你用过 flask 你一定知道 Werkzeug，一套 flask 依赖的 WSGI 工具集。我们换用 Werkzeug 来写上面的应用几乎一样:\n",
    "``` python\n",
    "from werkzeug.wrappers import Request, Response\n",
    "\n",
    "@Request.application\n",
    "def application(request):\n",
    "    name = request.args.get('name', 'PyCon')\n",
    "    return Response(['<h1>hello {name}</h1>'.format(name=name)])\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "# <center> 路由 Router </center>\n",
    "大部分 web 框架实现了基于匹配的路由，将 url 模式与一个可调用对象匹配，比如 flask 的路由方式:\n",
    "\n",
    "``` python\n",
    "@app.route(\"/user/<username>/photos/<int:photo_id>\")\n",
    "def detail(username, photo_id):\n",
    "    # ...\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "outputs": [],
   "source": [
    "# Router 实现方式如下，维护一个请求路径到可调用对象的 tuple 列表，\n",
    "# 每次从列表中查找请求路径是否满足当前 pattern， \n",
    "# 满足则调用当前 pattern 对应的可调用对象进行处理。否则抛个异常返回 404 response\n",
    "\n",
    "class NotFoundError(Exception):\n",
    "    \"\"\" url pattern not found \"\"\"\n",
    "    pass\n",
    "\n",
    "\n",
    "class Router(object):\n",
    "    def __init__(self):\n",
    "        self.routing_table = []    # 保存 url pattern 和 可调用对象\n",
    "\n",
    "    def add_route(self, pattern, callback):\n",
    "        self.routing_table.append((pattern, callback))\n",
    "\n",
    "    def match(self, path):\n",
    "        for (pattern, callback) in self.routing_table:\n",
    "            m = re.match(pattern, path)\n",
    "            if m:\n",
    "                return (callback, m.groups())\n",
    "        raise NotFoundError()\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "outputs": [],
   "source": [
    "def hello(request, name):\n",
    "    return Response(\"<h1>Hello, {name}</h1>\".format(name=name))\n",
    "\n",
    "\n",
    "def goodbye(request, name):\n",
    "    return Response(\"<h1>Goodbye, {name}</h1>\".format(name=name))\n",
    "\n",
    "\n",
    "routers = Router()\n",
    "routers.add_route(r'/hello/(.*)/$', hello)\n",
    "routers.add_route(r'/goodbye/(.*)/$', goodbye)\n",
    "\n",
    "def application(environ, start_response):\n",
    "    try:\n",
    "        request = Request(environ)\n",
    "        callback, args = routers.match(request.path)\n",
    "        response = callback(request, *args)\n",
    "    except NotFoundError:\n",
    "        response = Response(\"<h1>Not found</h1>\", status=404)\n",
    "    start_response(response.status, response.headers.items())\n",
    "    return iter(response)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "subslide"
    }
   },
   "outputs": [],
   "source": [
    "# 当然了，如果喜欢类似 flask 那样的装饰器实现，我们也可以使用类的 __call__ 方法(2_router_app.py):\n",
    "class DecoratorRouter(object):\n",
    "    def __init__(self):\n",
    "        self.routing_table = []    # 保存 url pattern 和 可调用对象\n",
    "\n",
    "    def match(self, path):\n",
    "        for (pattern, callback) in self.routing_table:\n",
    "            m = re.match(pattern, path)\n",
    "            if m:\n",
    "                return (callback, m.groups())\n",
    "        raise NotFoundError()\n",
    "\n",
    "    def __call__(self, pattern):\n",
    "        def _(func):\n",
    "            self.routing_table.append((pattern, func))\n",
    "        return _\n",
    "\n",
    "routers = DecoratorRouter()\n",
    "\n",
    "@routers(r'/hello/(.*)/$')\n",
    "def hello(request, name):\n",
    "    return Response(\"<h1>Hello, {name}</h1>\".format(name=name))\n",
    "\n",
    "\n",
    "@routers(r'/goodbye/(.*)/$')\n",
    "def goodbye(request, name):\n",
    "    return Response(\"<h1>Goodbye, {name}</h1>\".format(name=name))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "\"\"\" App 改成 class(3_app_class.py)\n",
    "只要实现 `__call__` 方法我们就可以把 application 改成类了:\n",
    "\"\"\"\n",
    "\n",
    "class Application(object):\n",
    "\n",
    "    def __init__(self, routers, **kwargs):\n",
    "        self.routers = routers\n",
    "        \n",
    "    def __call__(self, environ, start_response):\n",
    "        try:\n",
    "            request = Request(environ)\n",
    "            callback, args = routers.match(request.path)\n",
    "            response = callback(request, *args)\n",
    "        except NotFoundError:\n",
    "            response = Response(\"<h1>Not found</h1>\", status=404)\n",
    "        start_response(response.status, response.headers.items())\n",
    "        return iter(response)\n",
    "\n",
    "\n",
    "application = Application(routers)\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "# 实现 App 中间件 (4_app_middleware.py)\n",
    "# 有时候希望在请求之前或请求之后做一些通用的操作，比如记录日志、修改返回结果等\n",
    "# 可以通过中间件解决。比如我们想把所有的返回结果变成大写：\n",
    "\n",
    "class UppercaseMiddleware(object):\n",
    "    \n",
    "    def __init__(self, app):\n",
    "        self.app = app\n",
    "\n",
    "    def __call__(self, environ, start_response):\n",
    "        for data in self.app(environ, start_response):\n",
    "            yield data.upper()\n",
    "                \n",
    "application = UppercaseMiddleware(Application(routers))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "# <center>大功告成，用 gunicorn 启动应用</center>\n",
    "之前说到了 Wsgi 规范的好处，就是可以让我们的应用跑在各种兼容 Wsgi 规范的 http server 上，比如用 gunicorn 来启动我们的应用：\n",
    "\n",
    "```sh\n",
    "gunicorn app:application -b 0.0.0.0:8000 -w 4\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "# <center>gunicorn pre-fork</center>\n",
    "<center>![pre-forking](http://7ktuty.com1.z0.glb.clouddn.com/Screen%20Shot%202018-01-23%20at%2022.37.49.png)</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "\n",
    "# <center>QA</center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "# 参考: \n",
    "- [Wergzeug](http://werkzeug-docs-cn.readthedocs.io/zh_CN/latest/)\n",
    "- [PEP 3333 -- Python Web Server Gateway Interface v1.0.1](https://www.python.org/dev/peps/pep-3333/)\n",
    "- [Learn about WSGI](http://wsgi.readthedocs.io/en/latest/learn.html)\n",
    "- [LET’S BUILD A WEB FRAMEWORK!](https://jacobian.github.io/pycon2017/#/)"
   ]
  }
 ],
 "metadata": {
  "celltoolbar": "Slideshow",
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
